﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;

namespace DNX.IoCnoC
{
    public interface IDecoratorObjectBuilderFactory
    {
        IDecoratorObjectBuilder AddDecorator(Type type);
    }

    public class DecoratorObjectBuilderFactory : IDecoratorObjectBuilderFactory
    {
        public IDecoratorObjectBuilder AddDecorator(Type type)
        {
            return new DecoratorObjectBuilder().AddDecorator(type);
        }
    }

    public interface IDecoratorObjectBuilder
    {
        string GenerateCode();
        object Create(object inner, BehaviorAttribute behavior);
        Type Compile();
        DecoratorObjectInfo GetObjectInfo();
    }

    public class DecoratorObjectBuilder : IDecoratorObjectBuilder
    {
        private const string RootNamespace = "DNX.IoConC.Dynamic.Decorators";
        private readonly CodeCompileUnit targetUnit;
        private readonly CodeTypeDeclaration targetClass;
        private readonly CodeNamespace unit;
        private readonly CompilerParameters compilerParameters;
        private CodeConstructor constructor;
        
        public DecoratorObjectBuilder()
        {
            targetUnit = new CodeCompileUnit();
            unit = new CodeNamespace(RootNamespace);
            targetClass = new CodeTypeDeclaration
            {
                IsClass = true,
                TypeAttributes = TypeAttributes.Public,
            };
            unit.Types.Add(targetClass);
            targetUnit.Namespaces.Add(unit);

            compilerParameters = new CompilerParameters
            {
                IncludeDebugInformation = true,
                GenerateInMemory = true,
            };
        }

        private void AddReferencedAssembly(Type type)
        {
            compilerParameters.ReferencedAssemblies.Add(Assembly.GetAssembly(type).ManifestModule.Name);
        }

        private static string FullName(Type type)
        {
            return string.Format("{0}_Decorator", (type.IsArray ? type.GetElementType().FullName + "_array" : type.FullName).Replace(".", "_"));
        }

        public IDecoratorObjectBuilder AddDecorator(Type type)
        {
            targetClass.Name = FullName(type);
            
            DefineInterfaces(type);
            AddConstructor();
            CreateParameter(type, "inner");
            CreateParameter(typeof(BehaviorAttribute), "behavior");
            AddReferencedAssembly(typeof(MethodInfo));
            AddReferencedAssembly(typeof(Intercepted));
            
            foreach (var methodInfo in type.GetInterfaces().SelectMany(i=>i.GetMethods()))
                CreateProxyMethod(methodInfo, type);
            return this;
        }

        private void CreateProxyMethod(MethodInfo methodInfo, Type type)
        {
            var method = DefineMethod(methodInfo);
            DefineParameters(methodInfo, method);
            DefineProxyMethod(methodInfo);
            DefineIntercepeted(methodInfo, method);
            InitializeIntercepted(methodInfo, method, type);
            DefineInterpectorCall(method);
            DefineReturnType(methodInfo, method);
        }

        private void DefineProxyMethod(MethodInfo methodInfo)
        {
            var method = new CodeMemberMethod
            {
                Attributes = MemberAttributes.Private,
                Name = ProxyMethodFormat(methodInfo)
            };
            targetClass.Members.Add(method);
            var intercepted = new CodeParameterDeclarationExpression(typeof(IIntercepted), "intercepted");
            method.Parameters.Add(intercepted);

            var paramters = new List<CodeExpression>();
            var index = 0;
            foreach (var parameter in methodInfo.GetParameters())
            {
                paramters.Add(new CodeCastExpression(parameter.ParameterType, new CodeVariableReferenceExpression("intercepted.Parameters[" + index + "]")));
                index++;
            }

            var codeMethodInvokeExpression = new CodeMethodInvokeExpression(
                new CodeFieldReferenceExpression(
                    new CodeThisReferenceExpression(),
                    "inner"),
                methodInfo.Name, paramters.ToArray());

            if (IsVoidMethod(methodInfo))
            {
                method.Statements.Add(codeMethodInvokeExpression);
                return;
            }
            method.Statements.Add(new CodeAssignStatement(new CodeVariableReferenceExpression("intercepted.Return"),codeMethodInvokeExpression));
        }

        private static string ProxyMethodFormat(MethodInfo methodInfo)
        {
            return string.Format("{0}_Proxy_{1}", methodInfo.Name,methodInfo.MetadataToken);
        }

        private CodeMemberMethod DefineMethod(MethodInfo methodInfo)
        {
            var method = new CodeMemberMethod
                {
                    Attributes = MemberAttributes.Public,
                    Name = methodInfo.Name,
                    ReturnType = new CodeTypeReference(methodInfo.ReturnType),
                };
            targetClass.Members.Add(method);
            return method;
        }

        private static void DefineReturnType(MethodInfo methodInfo, CodeMemberMethod method)
        {
            if (!IsVoidMethod(methodInfo))
                method.Statements.Add(new CodeMethodReturnStatement
                    {
                        Expression =
                            new CodeCastExpression(methodInfo.ReturnType,
                                                   new CodeVariableReferenceExpression("intercepted.Return"))
                    });
        }

        private static bool IsVoidMethod(MethodInfo methodInfo)
        {
            return methodInfo.ReturnType == typeof (void);
        }

        private static void DefineInterpectorCall(CodeMemberMethod method)
        {
            method.Statements.Add(new CodeMethodInvokeExpression(
                                      new CodeFieldReferenceExpression(new CodeThisReferenceExpression(), "behavior"),
                                      "Execute",
                                      new CodeVariableReferenceExpression("intercepted")));
        }

        private static void InitializeIntercepted(MethodInfo methodInfo, CodeMemberMethod method, Type type)
        {
            method.Statements.Add(new CodeAssignStatement(
                                      new CodeVariableReferenceExpression("intercepted.Instance"),
                                      new CodePropertyReferenceExpression(new CodeThisReferenceExpression(), "inner")));

            method.Statements.Add(new CodeAssignStatement(
                                      new CodeVariableReferenceExpression("intercepted.MethodName"),
                                      new CodePrimitiveExpression(methodInfo.Name)));

            method.Statements.Add(new CodeAssignStatement(
                                      new CodeVariableReferenceExpression("intercepted.Namespace"),
                                      new CodePrimitiveExpression(type.Namespace)));

            method.Statements.Add(new CodeAssignStatement(
                                      new CodeVariableReferenceExpression("intercepted.Parameters"),
                                      new CodeArrayCreateExpression(typeof(object),
                                                                    methodInfo.GetParameters()
                                                                              .Select(
                                                                                  parameterInfo =>
                                                                                  new CodeVariableReferenceExpression(
                                                                                      parameterInfo.Name))
                                                                              .ToArray())));

            method.Statements.Add(new CodeAssignStatement(
                                      new CodeVariableReferenceExpression("intercepted.ParameterTypes"),
                                      new CodeArrayCreateExpression(typeof (Type),
                                                  methodInfo.GetParameters()
                                                            .Select(
                                                                parameterInfo =>
                                                                    new CodeTypeOfExpression(parameterInfo.ParameterType)
                                                                )
                                                            .ToArray())));

            
        }

        private static void DefineIntercepeted(MethodInfo methodInfo, CodeMemberMethod method)
        {
            var intercepted = new CodeVariableDeclarationStatement(
                typeof (Intercepted),
                "intercepted",
                new CodeObjectCreateExpression(
                    typeof (Intercepted),
                    new CodeDelegateCreateExpression(
                        new CodeTypeReference(typeof (Action<IIntercepted>)),
                        new CodeThisReferenceExpression(),
                        ProxyMethodFormat(methodInfo))));
            method.Statements.Add(intercepted);
        }

        private static void DefineParameters(MethodInfo methodInfo, CodeMemberMethod method)
        {
            method.Parameters.AddRange(
                methodInfo.GetParameters()
                          .Select(i => new CodeParameterDeclarationExpression(i.ParameterType, i.Name))
                          .ToArray());
        }

        private void AddConstructor()
        {
            constructor = new CodeConstructor {Attributes = MemberAttributes.Public};
            targetClass.Members.Add(constructor);
        }

        private void DefineInterfaces(Type type)
        {
            foreach (var @interface in type.GetInterfaces())
                AddReferencedAssembly(@interface);
            
            targetClass.BaseTypes.AddRange(type.GetInterfaces().Select(i => new CodeTypeReference(i)).ToArray());
        }

        private void CreateParameter(Type type, string parameterName)
        {
            AddReferencedAssembly(type);
            constructor.Parameters.Add(new CodeParameterDeclarationExpression(type, parameterName));
            var innerReference =
                new CodeFieldReferenceExpression(
                    new CodeThisReferenceExpression(), parameterName);
            constructor.Statements.Add(new CodeAssignStatement(innerReference,
                                                               new CodeArgumentReferenceExpression(parameterName)));

            var innerField = new CodeMemberField
                {
                    Attributes = MemberAttributes.Private,
                    Name = parameterName,
                    Type = new CodeTypeReference(type)
                };
            targetClass.Members.Add(innerField);
        }

        public string GenerateCode()
        {
            var provider = CodeDomProvider.CreateProvider("CSharp");
            var options = new CodeGeneratorOptions { BracingStyle = "C" };
            using (var stringWriter = new StringWriter())
            {
                provider.GenerateCodeFromCompileUnit(targetUnit, stringWriter, options);
                var stringBuilder = stringWriter.GetStringBuilder();
                var autogenerated = stringWriter.ToString();
                stringBuilder.Remove(0, autogenerated.IndexOf("namespace", StringComparison.Ordinal));
                return stringWriter.ToString();
            }
        }

        public Type Compile()
        {
            compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            compilerParameters.ReferencedAssemblies.Add("System.dll");
            var compilerResults = CodeDomProvider.CreateProvider("CSharp").CompileAssemblyFromDom(compilerParameters, targetUnit);
            return compilerResults.CompiledAssembly.GetType(string.Format("{0}.{1}", RootNamespace, targetClass.Name));
        }

        public DecoratorObjectInfo GetObjectInfo()
        {
            compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            compilerParameters.ReferencedAssemblies.Add("System.dll");
            var result = new DecoratorObjectInfo
                {
                    Unit = targetUnit,
                    Name = targetClass.Name,
                    Namespace = RootNamespace
                };
            return result;
        }

        public object Create(object inner, BehaviorAttribute behavior)
        {
         
            compilerParameters.ReferencedAssemblies.Add("mscorlib.dll");
            compilerParameters.ReferencedAssemblies.Add("System.dll");

            var compilerResults = CodeDomProvider.CreateProvider("CSharp").CompileAssemblyFromDom(compilerParameters, targetUnit);
            
            return compilerResults.CompiledAssembly.CreateInstance(
                string.Format("{0}.{1}", RootNamespace, targetClass.Name),
                false,
                BindingFlags.Default, 
                null,
                new[]{inner,behavior},
                CultureInfo.InvariantCulture, 
                new object[0]
                );
        }
    }
}



